Resumen ejecutivo
Vimeo expone una API REST (v3.x) bajo https://api.vimeo.com
y usa OAuth 2.0 (tokens Bearer) para autenticación. Hay tres caminos frecuentes:
- Token autenticado (Personal Access Token): asociado a tu cuenta; ideal para backends que sólo operan tu cuenta.
- OAuth 2.0 Authorization Code: obtiene access token (y refresh token) con consentimiento del usuario final.
- Client Credentials: token de app (sólo datos públicos); no sirve para subir ni administrar videos privados.
Subidas soportan tres enfoques: TUS (resumible), Pull (Vimeo descarga desde una URL), y Form/POST (legado). Recomendado: TUS. Además, los estados de upload/transcode se consultan por API (no hay webhook de “transcode completado” en la API estándar).
Mapa de servicios & capacidades
- Videos: crear, subir, actualizar metadatos/privacidad, obtener enlaces de reproducción/archivos.
- Text tracks (subtítulos), thumbnails, folders/proyectos, álbumes, oEmbed, Live/HLS (planes compatibles).
- Límites: cabeceras
X-RateLimit-Limit
,X-RateLimit-Remaining
,X-RateLimit-Reset
; ante excedentes, HTTP 429.
Requisitos previos
- Cuenta Vimeo y App creada en Developer (Client ID/Secret).
- Upload access aprobado para el scope
upload
si vas a subir por API. - Generar Personal Access Token o implementar OAuth 2.0 (authorization code).
- Servidor PHP 8.x con
curl
,json
y almacenamiento seguro de secretos.
Esquema MySQL base
-- Tokens (S2S con personal token o tokens OAuth por usuario)
CREATE TABLE vimeo_tokens (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
owner VARCHAR(80) NOT NULL, -- 'APP' para token personal, o email/user_id
access_token VARCHAR(500) NOT NULL,
refresh_token VARCHAR(500), -- sólo para OAuth authorization code
scopes TEXT,
expires_at DATETIME, -- NULL para personal tokens sin expiración
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
UNIQUE KEY uq_owner(owner)
);
-- Registro de videos
CREATE TABLE vimeo_videos (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
vimeo_uri VARCHAR(100), -- p. ej., "/videos/123456789"
vimeo_id BIGINT, -- 123456789
name VARCHAR(255),
status ENUM('creating','uploading','uploaded','transcoding','ready','error') NOT NULL,
link TEXT, -- https://vimeo.com/123456789
player_embed_url TEXT, -- https://player.vimeo.com/video/123456789
privacy_view VARCHAR(40), -- anybody, unlisted, password, nobody, etc.
last_check_at DATETIME,
meta JSON,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL
);
-- Auditoría de llamadas (útil para troubleshooting y rate limits)
CREATE TABLE vimeo_calls (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
method VARCHAR(10), -- GET/POST/PATCH/DELETE
endpoint VARCHAR(255),
http_status INT,
rate_limit INT,
rate_remaining INT,
rate_reset INT,
req_body MEDIUMTEXT,
resp_body MEDIUMTEXT,
created_at DATETIME NOT NULL
);
Utilitarios PHP
<?php
// Variables de entorno recomendadas:
// VIMEO_CLIENT_ID, VIMEO_CLIENT_SECRET, VIMEO_PERSONAL_TOKEN (opcional)
$V_CLIENT_ID = getenv('VIMEO_CLIENT_ID');
$V_CLIENT_SECRET = getenv('VIMEO_CLIENT_SECRET');
$V_PERSONAL = getenv('VIMEO_PERSONAL_TOKEN'); // si usas token personal
function http_json($method, $url, $headers = [], $payload = null){
$ch = curl_init($url);
$h = array_merge(['Accept: application/json'], $headers);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => $h,
CURLOPT_RETURNTRANSFER=> true,
CURLOPT_TIMEOUT => 120,
]);
if ($payload !== null){
$body = is_string($payload) ? $payload : json_encode($payload, JSON_UNESCAPED_SLASHES);
$h[] = 'Content-Type: application/json';
curl_setopt($ch, CURLOPT_HTTPHEADER, $h);
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
}
$resp = curl_exec($ch);
$http = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$hdrs = curl_getinfo($ch);
$err = curl_error($ch);
curl_close($ch);
if ($resp === false){ throw new RuntimeException("cURL error: $err"); }
return [$http, $resp, $hdrs];
}
function bearer($token){ return 'Authorization: Bearer '.$token; }
/** Client Credentials: sólo datos públicos */
function vimeo_token_client_credentials($scopes = 'public'){
global $V_CLIENT_ID, $V_CLIENT_SECRET;
$basic = base64_encode($V_CLIENT_ID.':'.$V_CLIENT_SECRET);
$url = 'https://api.vimeo.com/oauth/authorize/client';
$payload = ['grant_type' => 'client_credentials', 'scope' => $scopes];
list($http,$resp) = http_json('POST',$url,['Authorization: Basic '.$basic], $payload);
if ($http >= 400) throw new RuntimeException("Token client_credentials HTTP $http: $resp");
$data = json_decode($resp,true);
return [$data['access_token'],$data['scope'] ?? ''];
}
/** Intercambio authorization code → access/refresh token */
function vimeo_token_authorization_code($code, $redirect_uri){
global $V_CLIENT_ID, $V_CLIENT_SECRET;
$basic = base64_encode($V_CLIENT_ID.':'.$V_CLIENT_SECRET);
$url = 'https://api.vimeo.com/oauth/access_token';
$payload = [
'grant_type' => 'authorization_code',
'code' => $code,
'redirect_uri' => $redirect_uri
];
// Enviar como application/x-www-form-urlencoded:
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['Authorization: Basic '.$basic, 'Content-Type: application/x-www-form-urlencoded'],
CURLOPT_POSTFIELDS => http_build_query($payload),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 60
]);
$resp = curl_exec($ch);
$http = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($http >= 400) throw new RuntimeException("Token authorization_code HTTP $http: $resp");
return json_decode($resp,true); // access_token, refresh_token, scope, expires_in...
}
?>
Subida de video con TUS (resumible) – PHP
- Crear el recurso de video:
POST /me/videos
conupload.approach="tus"
yupload.size
(bytes). Respuesta incluyeupload.upload_link
(URL TUS) yuri
del video. - Enviar bytes a
upload_link
con métodoPATCH
, cabecerasTus-Resumable: 1.0.0
,Upload-Offset
, y cuerpo application/offset+octet-stream. Puedes subir en uno o varios chunks. - Confirmar con
HEAD upload_link
verificando queUpload-Offset
== tamaño. - Monitorear por API:
GET /videos/{id}?fields=uri,upload.status,transcode.status
hasta quetranscode.status="complete"
.
<?php
/**
* Subida TUS simplificada (archivo único en memoria; para grandes, fragmenta en chunks).
* Requiere token con scopes: upload, edit (y usualmente private/public).
*/
function vimeo_upload_tus($accessToken, $filePath, $name, $description = '', $privacyView = 'unlisted'){
$size = filesize($filePath);
if ($size === false) throw new RuntimeException('No se pudo leer tamaño de archivo');
// 1) Crear video
$payload = [
'upload' => ['approach' => 'tus', 'size' => $size],
'name' => $name,
'description' => $description,
'privacy'=> ['view' => $privacyView]
];
list($http, $resp) = http_json('POST','https://api.vimeo.com/me/videos', [bearer($accessToken)], $payload);
if ($http >= 400) throw new RuntimeException("Crear video HTTP $http: $resp");
$data = json_decode($resp,true);
$uploadLink = $data['upload']['upload_link'] ?? null;
$videoUri = $data['uri'] ?? null; // p.ej. "/videos/123456789"
if (!$uploadLink || !$videoUri) throw new RuntimeException('Respuesta sin upload_link/uri');
// 2) PATCH TUS (subida en un solo envío)
$fh = fopen($filePath, 'rb');
$binary = stream_get_contents($fh);
fclose($fh);
$ch = curl_init($uploadLink);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'PATCH',
CURLOPT_HTTPHEADER => [
'Tus-Resumable: 1.0.0',
'Upload-Offset: 0',
'Content-Type: application/offset+octet-stream',
'Content-Length: '.$size
],
CURLOPT_POSTFIELDS => $binary,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 0 // para subidas largas
]);
$resp2 = curl_exec($ch);
$http2 = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if (!in_array($http2,[204,201,200])) throw new RuntimeException("TUS PATCH HTTP $http2: ".$resp2);
// 3) HEAD para confirmar offset
$ch = curl_init($uploadLink);
curl_setopt_array($ch, [
CURLOPT_NOBODY => true,
CURLOPT_CUSTOMREQUEST => 'HEAD',
CURLOPT_HTTPHEADER => ['Tus-Resumable: 1.0.0'],
CURLOPT_RETURNTRANSFER => true
]);
curl_exec($ch);
$offset = 0;
if (preg_match('/Upload-Offset:\s*(\d+)/i', implode("\n", headers_list()), $m)) {
$offset = (int)$m[1];
}
curl_close($ch);
// 4) Devuelve id y uri; monitorea transcode aparte
$videoId = (int)basename($videoUri);
return [$videoId, $videoUri];
}
?>
Notas: para archivos grandes, sube por chunks (mantén y actualiza Upload-Offset
). Implementa reintentos idempotentes y verifica el offset antes de reanudar.
Subida por Pull (Vimeo descarga desde una URL)
<?php
$payload = [
'upload' => ['approach' => 'pull', 'link' => 'https://mi-cdn.example.com/video.mp4'],
'name' => 'Demo Pull Upload',
'privacy'=> ['view' => 'unlisted']
];
list($http,$resp) = http_json('POST','https://api.vimeo.com/me/videos',[bearer($V_PERSONAL)],$payload);
if ($http >= 400) { throw new RuntimeException("Pull upload HTTP $http: $resp"); }
$data = json_decode($resp,true);
$videoId = (int)basename($data['uri']);
?>
Monitoreo de upload/transcode
Consulta periódicamente el estado del video tras la subida. Campos clave: upload.status
y transcode.status
con valores in_progress, complete o error.
<?php
$fields = 'uri,link,player_embed_url,upload.status,transcode.status';
$url = "https://api.vimeo.com/videos/$videoId?fields=".rawurlencode($fields);
list($http,$resp) = http_json('GET', $url, [bearer($V_PERSONAL)]);
if ($http === 200) {
$v = json_decode($resp,true);
$statusUpload = $v['upload']['status'] ?? '';
$statusTrans = $v['transcode']['status'] ?? '';
$embedUrl = $v['player_embed_url'] ?? null;
}
?>
Importante: la API estándar no emite webhook de “transcode completado”; debes polling hasta transcode.status="complete"
antes de exponer el video al usuario.
Enlaces de reproducción (player y archivos)
Con el scope video_files
puedes obtener URLs de archivos/streams (HLS/MP4) para reproductores externos; también dispones de link
y player_embed_url
para iframes.
<?php
$fields = 'link,player_embed_url,files';
$url = "https://api.vimeo.com/videos/$videoId?fields=".rawurlencode($fields);
list($http,$resp) = http_json('GET',$url,[bearer($V_PERSONAL)]);
$video = json_decode($resp,true);
$embed = $video['player_embed_url'] ?? null; // iframe src
$files = $video['files'] ?? []; // requiere scope video_files
?>
oEmbed (generar iframe desde una URL pública)
Si ya tienes la URL del video (https://vimeo.com/{id}
), puedes generar HTML de embed vía oEmbed.
Privacidad y contraseñas
Ajusta la privacidad del video (p. ej. anybody, unlisted, password, nobody) y, si corresponde, define password. La API permite establecerla (no exponerla en lecturas públicas).
<?php
$payload = [
'name' => 'Título actualizado',
'privacy' => ['view' => 'password'],
'password' => 'MiPassFuerte123'
];
list($http,$resp) = http_json('PATCH', "https://api.vimeo.com/videos/$videoId",
[bearer($V_PERSONAL)], $payload);
if ($http >= 400) { /* manejar error */ }
?>
Paso a paso de implementación
Crea la app y define scopes
- Genera Personal Access Token (si la app operará tu cuenta) con
public
,private
,edit
,upload
(yvideo_files
si necesitas URLs de archivos). - Si tu app actuará por usuarios, implementa OAuth 2.0 (authorization code) y almacena refresh token.
- Si sólo lees datos públicos, usa client credentials (scope
public
).
Solicita Upload Access
- Desde la página de tu App en Developer, pide aprobación para el scope
upload
(revisión manual).
Sube y monitorea
- Usa TUS para robustez (reanudación por offset).
- Tras subir, realiza polling de
transcode.status
hasta complete.
Entrega y privacidad
- Decide privacy.view (unlisted/password/etc.) y restringe embed por dominio si corresponde.
- Si necesitas archivos directos (HLS/MP4), añade el scope
video_files
.
Operación
- Respeta límites (lee cabeceras de rate limit) y aplica backoff ante 429.
- Audita respuestas y estados para soporte y métricas.
Rate limits y manejo de errores
Síntoma | Causa | Acción |
---|---|---|
HTTP 401 | Token inválido/expirado o token de app sin permisos | Renueva token; usa token autenticado con scopes correctos. |
HTTP 403 | Falta scope (p. ej. upload , edit ) o falta aprobación de upload | Ajusta scopes de la app; solicita upload access. |
HTTP 429 | Límite de tasa superado | Lee X-RateLimit-Remaining y X-RateLimit-Reset ; aplica reintentos exponenciales. |
Video sin miniatura/“no reproducible” tras subir | Transcodificación en progreso | Haz polling de transcode.status hasta complete antes de publicar. |
Seguridad & buenas prácticas
- Guarda Client Secret y tokens fuera del árbol web; rota secretos periódicamente.
- Para webhooks propios (si integras herramientas intermedias), valida firmas HMAC y rechaza replays.
- Minimiza scopes (principio de privilegio mínimo).
Anexos & referencias
- Autenticación OAuth 2.0 en Vimeo.
- Guías de inicio y formatos comunes (cabecera Authorization Bearer).
- Subidas de video: TUS, Pull, requisitos y estados.
- Transcode status por API (no hay webhook estándar de “done”).
- Enlaces de archivos de video (scope
video_files
). - oEmbed y reproducción en players externos/HLS (Live).
- Rate limiting (cabeceras y manejo de 429).
- Privacidad y contraseña por API.
- Especificación OpenAPI 3.4 (referencia de endpoints).
Consulta siempre la documentación vigente de Vimeo y tu plan de cuenta; ciertos endpoints/características requieren planes superiores o aprobaciones adicionales.